package agents;

import gui.AgvGui;
import jade.core.AID;
import jade.core.behaviours.CyclicBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.FailureException;
import jade.domain.FIPAAgentManagement.NotUnderstoodException;
import jade.domain.FIPAAgentManagement.RefuseException;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.gui.GuiAgent;
import jade.gui.GuiEvent;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;
import jade.proto.ContractNetResponder;

import java.io.IOException;
import java.util.HashMap;

import model.Plan;
import model.Position;
import model.Product;
import model.Stage;
import model.Step;
import utility.Pair;
import agentInterface.AGVDetails;

public class AgvAgent extends GuiAgent {
	private static final long serialVersionUID = 1L;

	private AID factory;
	private AgvGui myGui;

	private Position position;
	private Plan plan = new Plan();
	private Product currentProduct = null;
	private float currentLoad = 0;
	private float maximumLoad = 300;
	private float currentBattery = 200;
	private float maximumBattery = 300;
	private int charges = 0;

	private double IMPOSSIBLE = 100000000;

	HashMap<AID, Position> powerPositions = new HashMap<AID, Position>();

	public AgvAgent(Position position) {
		this.position = position;
	}

	protected void setup() {
		System.out.println(getLocalName() + ": Hello World!");

		registerOnDF();

		getFactory();

		initGui();

		performMovement();

		answerMachines();

		receivePowerPositions();
	}

	private void answerMachines() {
		MessageTemplate template = MessageTemplate
				.and(MessageTemplate
						.MatchProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET),
						MessageTemplate.MatchPerformative(ACLMessage.CFP));

		addBehaviour(new ContractNetResponder(this, template) {
			protected ACLMessage handleCfp(ACLMessage cfp)
					throws NotUnderstoodException, RefuseException {
				// System.out.println("Agent " + getLocalName()
				// + ": CFP received from " + cfp.getSender().getName()
				// + ". Action is a plan");
				Step step = null;
				try {
					step = (Step) cfp.getContentObject();
				} catch (UnreadableException e) {
					e.printStackTrace();
				}

				float proposal = evaluateAction(step);
				if (proposal > 2) {
					// We provide a proposal
					System.out.println(getLocalName() + "@"
							+ cfp.getSender().getLocalName() + ": "
							+ ": I'm an available agv " + proposal
							+ "km from you (answering CFP with a propose)");
					ACLMessage propose = cfp.createReply();
					propose.setPerformative(ACLMessage.PROPOSE);
					propose.setContent(String.valueOf(proposal));
					return propose;
				} else {
					// We refuse to provide a proposal
					System.out.println(getLocalName() + "@"
							+ cfp.getSender().getLocalName() + ": "
							+ ": I can't go (answering CFP with refuse)");
					throw new RefuseException("evaluation-failed");
				}
			}

			protected ACLMessage handleAcceptProposal(ACLMessage cfp,
					ACLMessage propose, ACLMessage accept)
					throws FailureException {

				Step step = null;
				try {
					step = (Step) cfp.getContentObject();
				} catch (UnreadableException e) {
					e.printStackTrace();
				}

				// System.out.println("Agent " + getLocalName()
				// + ": Proposal accepted");
				if (performAction(step)) {
					// System.out.println("Agent " + getLocalName()
					// + ": Action successfully performed");
					ACLMessage inform = accept.createReply();
					inform.setPerformative(ACLMessage.INFORM);
					return inform;
				} else {
					System.out.println("Agent " + getLocalName()
							+ ": Action execution failed");
					throw new FailureException("unexpected-error");
				}
			}

			protected void handleRejectProposal(ACLMessage cfp,
					ACLMessage propose, ACLMessage reject) {
				System.out.println(getLocalName() + ": I got declined from "
						+ cfp.getSender().getLocalName());
			}
		});
	}

	private float evaluateAction(Step step) {
		float totalDistance = 0f;

		totalDistance = plan.getTotalDistancePlusRecharge(step, position,
				closestPowerPositionTo(step.getDestination()).getSecond());
		// evaluate product first, recharge next
		if ((enoughBattery(totalDistance) && enoughSpace(step.getProduct()))) {
			System.out.println("product dispatch possible");
			System.out.println(getLocalName() + ": my plan is "
					+ plan.toString());

			return totalDistance;
		} else {
			// recharge and then evaluate product
			charges++;
			Product battery = new Product("Battery", 0, 0, 0, Stage.dispatch);

			// closest recharge spot in regards to the last step in the agvs, if
			// the agv doesnt have it, it should use the currentposition
			Pair<AID, Position> rechargeSpot;
			try{
				rechargeSpot = closestPowerPositionTo(plan.getLastStep()
						.getDestination());
			} catch(Exception e) {
				rechargeSpot = closestPowerPositionTo(position);
			}
			Step recharge = new Step(null, rechargeSpot.getFirst(), null,
					rechargeSpot.getSecond(), battery);
			plan.addStep(recharge);
			totalDistance = plan.getTotalDistance(step, position);
			System.out
					.println(getLocalName()
							+ ": After receiving this propose of plan, I will recharge");
			if(!plan.toString().equals(""))
				System.out.println(getLocalName() + ": my plan is " + plan.toString());
			return totalDistance;
		}

	}

	private boolean enoughSpace(Product product) {
		if ((maximumLoad - currentLoad) > product.getWeight())
			return true;
		else
			return false;
	}

	private boolean enoughBattery(float totalDistance) {
		if ((currentBattery + charges * maximumBattery) > totalDistance) {
			// System.out.println("expected battery:"
			// + (currentBattery + recharging * maximumBattery)
			// + " | totaldistance: " + totalDistance);
			return true;
		} else
			return false;
	}

	private boolean performAction(Step s) {
		// make the agv move to the target position

		if (s != null) {
			// System.out.println(getLocalName() + " received a valid step");
			plan.addStep(s);
			informDestinationMachine(s);
		} else {
			System.out.println(getLocalName()
					+ " DID NOT received a valid step");
		}

		return true;
	}

	private void informDestinationMachine(Step s) {
		ACLMessage msg = new ACLMessage(ACLMessage.CONFIRM);
		try {
			msg.setContentObject(s.getProduct());
		} catch (IOException e) {
			e.printStackTrace();
		}

		msg.addReceiver(s.getDestinationID());
		send(msg);
	}

	private Pair<AID, Position> closestPowerPositionTo(Position p) {
		float min = 1000f;
		Pair<AID, Position> pair = null;
		for (AID key : powerPositions.keySet()) {
			float distance = p.findDistance(powerPositions.get(key));
			if (distance < min) {
				pair = new Pair<AID, Position>(key, powerPositions.get(key));
				min = distance;
			}
		}
		return pair;
	}

	private float getClosestPowerDistance() {
		Position p2 = closestPowerPosition().getSecond();
		return position.findDistance(p2);
	}

	private Pair<AID, Position> closestPowerPosition() {
		return closestPowerPositionTo(position);
	}

	private void receivePowerPositions() {
		addBehaviour(new CyclicBehaviour(this) {
			private static final long serialVersionUID = 1L;

			@Override
			public void action() {
				ACLMessage msg = receive(MessageTemplate
						.MatchPerformative(ACLMessage.INFORM));
				if (msg != null) {

					try {
						powerPositions.put(msg.getSender(),
								(Position) msg.getContentObject());
						System.out.println(getLocalName()
								+ ": I've receive the location of "
								+ msg.getSender().getLocalName());
					} catch (UnreadableException e) {
						e.printStackTrace();
					}

				}
			}
		});

	}

	private void performMovement() {

		addBehaviour(new TickerBehaviour(this, 10) {
			private static final long serialVersionUID = 1L;

			@Override
			protected void onTick() {
				if (plan.hasSteps()) {
					Position nextStop;
					if (plan.getSource() != null)
						nextStop = plan.getSource();
					else
						nextStop = plan.getDestination();

					if (position.findDistance(nextStop) < 0.001) {
						if (plan.getSource() != null) {
							pickupProduct();
						} else {
							dropofProduct();
						}
					} else {
						Position step = position.getMovementVector(nextStop);
						currentBattery -= step.getScalar();
						Move(position.goVector(step));
					}

				} else {

					//MoveRandomly();

				}
			}

			private void dropofProduct() {
				sendDropoffMessage();
				if (powerPositions.containsKey(plan.getCurrentStep()
						.getDestinationID())) {
					currentBattery = maximumBattery;
					charges--;
				} else {
					currentLoad -= currentProduct.getWeight();

				}
				currentProduct = null;
				myGui.refreshProduct();
				myGui.refreshLoad();
				myGui.refreshPosition();
				updateFactory();
				plan.dropStep();

			}

			private void pickupProduct() {
				sendPickupMessage();
				plan.getProduct().incrementStage();
				currentProduct = plan.getProduct();
				currentLoad += currentProduct.getWeight();
				myGui.refreshProduct();
				myGui.refreshLoad();
				plan.pickupPerformed();

			}

			private void sendDropoffMessage() {
				// we have to send a message to the machine, telling that
				// we want it to initiate a new processing
				// this message might have msg.setContentObject(plan.getProduct)
				System.out.println(getLocalName() + "@"
						+ plan.getDestinationID().getLocalName()
						+ ": I'm here, take a " + plan.getProduct().getName());
				ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
				try {
					msg.setContentObject(plan.getProduct());
				} catch (IOException e) {
					e.printStackTrace();
				}
				msg.addReceiver(plan.getDestinationID());
				send(msg);
			}

			private void sendPickupMessage() {
				System.out.println(getLocalName() + "@"
						+ plan.getSourceID().getLocalName()
						+ ": I'm here, give me a "
						+ plan.getProduct().getName());
				ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
				// msg.setContentObject(plan.getProduct());
				msg.setContent("pickup");
				msg.addReceiver(plan.getSourceID());
				send(msg);
			}

			private void MoveRandomly() {
				int a = (int) (Math.random() * 4);
				Position p;
				switch (a) {
				case 0:
					p = position.goRight();
					break;
				case 1:
					p = position.goLeft();
					break;
				case 2:
					p = position.goUp();
					break;
				default:
					p = position.goDown();
					break;
				}

				if (p.inBounds())
					Move(p);
				else
					MoveRandomly();
			}
		});

	}

	private void Move(Position p) {
		this.position = p;

		updateFactory();

		myGui.refreshPosition();
	}

	private void updateFactory() {
		ACLMessage msg = new ACLMessage(ACLMessage.INFORM);
		try {
			msg.setContentObject(new AGVDetails(this));
		} catch (IOException e) {
			e.printStackTrace();
		}

		msg.addReceiver(factory);
		send(msg);
	}

	private void initGui() {
		myGui = new AgvGui(this);
		myGui.showGui();
	}

	private void getFactory() {
		try {
			DFAgentDescription template = new DFAgentDescription();
			ServiceDescription templateSd = new ServiceDescription();
			templateSd.setType("factory");
			template.addServices(templateSd);

			DFAgentDescription[] results = DFService.search(this, template);
			if (results.length > 0) {
				System.out.println("Agent " + getLocalName()
						+ " found the following factory services:");
				for (int i = 0; i < results.length; ++i) {
					DFAgentDescription dfd = results[i];
					AID provider = dfd.getName();
					factory = provider;
					// agvs.add(provider);
					System.out.println("\t - " + provider.getLocalName());

					// //The same agent may provide several services; we are
					// only interested
					// Iterator it = dfd.getAllServices();
					// while (it.hasNext()) {
					// ServiceDescription sd = (ServiceDescription) it.next();
					// if (sd.getType().equals("weather-forecast")) {
					// System.out.println("- Service \""+sd.getName()+"\" provided by agent "+provider.getName());
					// }
					// }
				}
			} else {
				System.out.println("Agent " + getLocalName()
						+ " did not find any factory service");
			}
		} catch (FIPAException fe) {
			fe.printStackTrace();
		}
	}

	private void registerOnDF() {
		// registar no DF
		DFAgentDescription dfd = new DFAgentDescription();

		ServiceDescription sd1 = new ServiceDescription();
		sd1.setName(getName());
		sd1.setType("agv");

		dfd.setName(getAID());
		dfd.addServices(sd1);

		try {
			DFService.register(this, dfd);
		} catch (FIPAException e) {
			System.out.println(getLocalName() + "Cannot register with DF");
			doDelete();
		}

	}

	@Override
	protected void onGuiEvent(GuiEvent arg0) {

	}

	public Position getPosition() {
		return position;
	}

	public float getCurrentLoad() {
		return currentLoad;
	}

	public float getMaximumLoad() {
		return maximumLoad;
	}

	public float getCurrentBattery() {
		return currentBattery;
	}

	public float getMaximumBattery() {
		return maximumBattery;
	}

	public Product getCurrentProduct() {
		return currentProduct;
	}
}
